/**
 * AJAX Upload ( http://valums.com/ajax-upload/ )
 * Copyright (c) Andris Valums
 * Licensed under the MIT license ( http://valums.com/mit-license/ )
 * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions
 */
/* global window */
/* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */

/**
 * Wrapper for FireBug's console.log
 */

function log() {
    if (typeof(console) != 'undefined' && typeof(console.log) == 'function') {
        Array.prototype.unshift.call(arguments, '[Ajax Upload]');
        console.log(Array.prototype.join.call(arguments, ' '));
    }
}

/**
 * Attaches event to a dom element.
 * @param {Element} el
 * @param type event name
 * @param fn callback This refers to the passed element
 */

function addEvent(el, type, fn) {
    if (el.addEventListener) {
        el.addEventListener(type, fn, false);
    } else if (el.attachEvent) {
        el.attachEvent('on' + type, function () {
            fn.call(el);
        });
    } else {
        throw new Error('not supported or DOM not loaded');
    }
}

/**
 * Attaches resize event to a window, limiting
 * number of event fired. Fires only when encounteres
 * delay of 100 after series of events.
 *
 * Some browsers fire event multiple times when resizing
 * http://www.quirksmode.org/dom/events/resize.html
 *
 * @param fn callback This refers to the passed element
 */

function addResizeEvent(fn) {
    var timeout;

    addEvent(window, 'resize', function () {
        if (timeout) {
            clearTimeout(timeout);
        }
        timeout = setTimeout(fn, 100);
    });
}

// Needs more testing, will be rewriten for next version
// getOffset function copied from jQuery lib (http://jquery.com/)
if (document.documentElement.getBoundingClientRect) {
    // Get Offset using getBoundingClientRect
    // http://ejohn.org/blog/getboundingclientrect-is-awesome/
    var getOffset = function (el) {
        var box = el.getBoundingClientRect();
        var doc = el.ownerDocument;
        var body = doc.body;
        var docElem = doc.documentElement; // for ie
        var clientTop = docElem.clientTop || body.clientTop || 0;
        var clientLeft = docElem.clientLeft || body.clientLeft || 0;

        // In Internet Explorer 7 getBoundingClientRect property is treated as physical,
        // while others are logical. Make all logical, like in IE8.
        var zoom = 1;
        if (body.getBoundingClientRect) {
            var bound = body.getBoundingClientRect();
            zoom = (bound.right - bound.left) / body.clientWidth;
        }

        if (zoom > 1) {
            clientTop = 0;
            clientLeft = 0;
        }

        var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop,
            left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft;

        return {
            top: top,
            left: left
        };
    };
} else {
    // Get offset adding all offsets
    var getOffset = function (el) {
        var top = 0,
            left = 0;
        do {
            top += el.offsetTop || 0;
            left += el.offsetLeft || 0;
            el = el.offsetParent;
        } while (el);

        return {
            left: left,
            top: top
        };
    };
}

/**
 * Returns left, top, right and bottom properties describing the border-box,
 * in pixels, with the top-left relative to the body
 * @param {Element} el
 * @return {Object} Contains left, top, right,bottom
 */

function getBox(el) {
    var left, right, top, bottom;
    var offset = getOffset(el);
    left = offset.left;
    top = offset.top;

    right = left + el.offsetWidth;
    bottom = top + el.offsetHeight;

    return {
        left: left,
        right: right,
        top: top,
        bottom: bottom
    };
}

/**
 * Helper that takes object literal
 * and add all properties to element.style
 * @param {Element} el
 * @param {Object} styles
 */

function addStyles(el, styles) {
    for (var name in styles) {
        if (styles.hasOwnProperty(name)) {
            el.style[name] = styles[name];
        }
    }
}

/**
 * Function places an absolutely positioned
 * element on top of the specified element
 * copying position and dimentions.
 * @param {Element} from
 * @param {Element} to
 */

function copyLayout(from, to) {
    var box = getBox(from);

    addStyles(to, {
        position: 'absolute',
        left: box.left + 'px',
        top: box.top + 'px',
        width: from.offsetWidth + 'px',
        height: from.offsetHeight + 'px'
    });
}

/**
 * Creates and returns element from html chunk
 * Uses innerHTML to create an element
 */
var toElement = (function () {
    var div = document.createElement('div');
    return function (html) {
        div.innerHTML = html;
        var el = div.firstChild;
        return div.removeChild(el);
    };
})();

/**
 * Function generates unique id
 * @return unique id
 */
var getUID = (function () {
    var id = 0;
    return function () {
        return 'ValumsAjaxUpload' + id++;
    };
})();

/**
 * Get file name from path
 * @param {String} file path to file
 * @return filename
 */

function fileFromPath(file) {
    return file.replace(/.*(\/|\\)/, "");
}

/**
 * Get file extension lowercase
 * @param {String} file name
 * @return file extenstion
 */

function getExt(file) {
    return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : '';
}

function hasClass(el, name) {
    var re = new RegExp('\\b' + name + '\\b');
    return re.test(el.className);
}

function addClass(el, name) {
    if (!hasClass(el, name)) {
        el.className += ' ' + name;
    }
}

function removeClass(el, name) {
    var re = new RegExp('\\b' + name + '\\b');
    el.className = el.className.replace(re, '');
}

function removeNode(el) {
    el.parentNode.removeChild(el);
}

/**
 * Easy styling and uploading
 * @constructor
 * @param button An element you want convert to
 * upload button. Tested dimentions up to 500x500px
 * @param {Object} options See defaults below.
 */
var AjaxUpload = function (button, options) {
    this._settings = {
        // Location of the server-side upload script
        action: 'upload.php',
        // File upload name
        name: 'userfile',
        // Additional data to send
        data: {},
        // Submit file as soon as it's selected
        autoSubmit: true,
        // The type of data that you're expecting back from the server.
        // html and xml are detected automatically.
        // Only useful when you are using json data as a response.
        // Set to "json" in that case.
        responseType: false,
        // Class applied to button when mouse is hovered
        hoverClass: 'hover',
        // Class applied to button when AU is disabled
        disabledClass: 'disabled',
        // When user selects a file, useful with autoSubmit disabled
        // You can return false to cancel upload
        onChange: function (file, extension) {
        },
        // Callback to fire before file is uploaded
        // You can return false to cancel upload
        onSubmit: function (file, extension) {
        },
        // Fired when file upload is completed
        // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!
        onComplete: function (file, response) {
        }
    };

    // Merge the users options with our defaults
    for (var i in options) {
        if (options.hasOwnProperty(i)) {
            this._settings[i] = options[i];
        }
    }

    // button isn't necessary a dom element
    if (button.jquery) {
        // jQuery object was passed
        button = button[0];
    } else if (typeof button == "string") {
        if (/^#.*/.test(button)) {
            // If jQuery user passes #elementId don't break it
            button = button.slice(1);
        }

        button = document.getElementById(button);
    }

    if (!button || button.nodeType !== 1) {
        throw new Error("Please make sure that you're passing a valid element");
    }

    if (button.nodeName.toUpperCase() == 'A') {
        // disable link
        addEvent(button, 'click', function (e) {
            if (e && e.preventDefault) {
                e.preventDefault();
            } else if (window.event) {
                window.event.returnValue = false;
            }
        });
    }

    // DOM element
    this._button = button;
    // DOM element
    this._input = null;
    // If disabled clicking on button won't do anything
    this._disabled = false;

    // if the button was disabled before refresh if will remain
    // disabled in FireFox, let's fix it
    this.enable();

    this._rerouteClicks();
};

// assigning methods to our class
AjaxUpload.prototype = {
    setData: function (data) {
        this._settings.data = data;
    },
    disable: function () {
        addClass(this._button, this._settings.disabledClass);
        this._disabled = true;

        var nodeName = this._button.nodeName.toUpperCase();
        if (nodeName == 'INPUT' || nodeName == 'BUTTON') {
            this._button.setAttribute('disabled', 'disabled');
        }

        // hide input
        if (this._input) {
            // We use visibility instead of display to fix problem with Safari 4
            // The problem is that the value of input doesn't change if it
            // has display none when user selects a file
            this._input.parentNode.style.visibility = 'hidden';
        }
    },
    enable: function () {
        removeClass(this._button, this._settings.disabledClass);
        this._button.removeAttribute('disabled');
        this._disabled = false;

    },
    /**
     * Creates invisible file input
     * that will hover above the button
     * <div><input type='file' /></div>
     */
    _createInput: function () {
        var self = this;

        var input = document.createElement("input");
        input.setAttribute('type', 'file');
        input.setAttribute('name', this._settings.name);

        addStyles(input, {
            'position': 'absolute',
            // in Opera only 'browse' button
            // is clickable and it is located at
            // the right side of the input
            'right': 0,
            'margin': 0,
            'padding': 0,
            'fontSize': '480px',
            'cursor': 'pointer'
        });

        var div = document.createElement("div");
        addStyles(div, {
            'display': 'block',
            'position': 'absolute',
            'overflow': 'hidden',
            'margin': 0,
            'padding': 0,
            'opacity': 0,
            // Make sure browse button is in the right side
            // in Internet Explorer
            'direction': 'ltr',
            //Max zIndex supported by Opera 9.0-9.2
            'zIndex': 2147483583
        });

        // Make sure that element opacity exists.
        // Otherwise use IE filter
        if (div.style.opacity !== "0") {
            if (typeof(div.filters) == 'undefined') {
                throw new Error('Opacity not supported by the browser');
            }
            div.style.filter = "alpha(opacity=0)";
        }

        addEvent(input, 'change', function () {

            if (!input || input.value === '') {
                return;
            }

            // Get filename from input, required
            // as some browsers have path instead of it
            var file = fileFromPath(input.value);

            if (false === self._settings.onChange.call(self, file, getExt(file))) {
                self._clearInput();
                return;
            }

            // Submit form when value is changed
            if (self._settings.autoSubmit) {
                self.submit();
            }
        });

        addEvent(input, 'mouseover', function () {
            addClass(self._button, self._settings.hoverClass);
        });

        addEvent(input, 'mouseout', function () {
            removeClass(self._button, self._settings.hoverClass);

            // We use visibility instead of display to fix problem with Safari 4
            // The problem is that the value of input doesn't change if it
            // has display none when user selects a file
            input.parentNode.style.visibility = 'hidden';

        });

        div.appendChild(input);
        document.body.appendChild(div);

        this._input = input;
    },
    _clearInput: function () {
        if (!this._input) {
            return;
        }

        // this._input.value = ''; Doesn't work in IE6
        removeNode(this._input.parentNode);
        this._input = null;
        this._createInput();

        removeClass(this._button, this._settings.hoverClass);
    },
    /**
     * Function makes sure that when user clicks upload button,
     * the this._input is clicked instead
     */
    _rerouteClicks: function () {
        var self = this;

        // IE will later display 'access denied' error
        // if you use using self._input.click()
        // other browsers just ignore click()

        addEvent(self._button, 'mouseover', function () {
            if (self._disabled) {
                return;
            }

            if (!self._input) {
                self._createInput();
            }

            var div = self._input.parentNode;
            copyLayout(self._button, div);
            div.style.visibility = 'visible';

        });


        // commented because we now hide input on mouseleave
        /**
         * When the window is resized the elements
         * can be misaligned if button position depends
         * on window size
         */
        //addResizeEvent(function(){
        //    if (self._input){
        //        copyLayout(self._button, self._input.parentNode);
        //    }
        //});

    },
    /**
     * Creates iframe with unique name
     * @return {Element} iframe
     */
    _createIframe: function () {
        // We can't use getTime, because it sometimes return
        // same value in safari :(
        var id = getUID();

        // We can't use following code as the name attribute
        // won't be properly registered in IE6, and new window
        // on form submit will open
        // var iframe = document.createElement('iframe');
        // iframe.setAttribute('name', id);

        var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');
        // src="javascript:false; was added
        // because it possibly removes ie6 prompt
        // "This page contains both secure and nonsecure items"
        // Anyway, it doesn't do any harm.
        iframe.setAttribute('id', id);

        iframe.style.display = 'none';
        document.body.appendChild(iframe);

        return iframe;
    },
    /**
     * Creates form, that will be submitted to iframe
     * @param {Element} iframe Where to submit
     * @return {Element} form
     */
    _createForm: function (iframe) {
        var settings = this._settings;

        // We can't use the following code in IE6
        // var form = document.createElement('form');
        // form.setAttribute('method', 'post');
        // form.setAttribute('enctype', 'multipart/form-data');
        // Because in this case file won't be attached to request
        var form = toElement('<form method="post" enctype="multipart/form-data"></form>');

        form.setAttribute('action', settings.action);
        form.setAttribute('target', iframe.name);
        form.style.display = 'none';
        document.body.appendChild(form);

        // Create hidden input element for each data key
        for (var prop in settings.data) {
            if (settings.data.hasOwnProperty(prop)) {
                var el = document.createElement("input");
                el.setAttribute('type', 'hidden');
                el.setAttribute('name', prop);
                el.setAttribute('value', settings.data[prop]);
                form.appendChild(el);
            }
        }
        return form;
    },
    /**
     * Gets response from iframe and fires onComplete event when ready
     * @param iframe
     * @param file Filename to use in onComplete callback
     */
    _getResponse: function (iframe, file) {
        // getting response
        var toDeleteFlag = false,
            self = this,
            settings = this._settings;

        addEvent(iframe, 'load', function () {

            if ( // For Safari
            iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||
            // For FF, IE
            iframe.src == "javascript:'<html></html>';") {
                // First time around, do not delete.
                // We reload to blank page, so that reloading main page
                // does not re-submit the post.

                if (toDeleteFlag) {
                    // Fix busy state in FF3
                    setTimeout(function () {
                            removeNode(iframe);
                        },
                        0);
                }

                return;
            }

            var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document;

            // fixing Opera 9.26,10.00
            if (doc.readyState && doc.readyState != 'complete') {
                // Opera fires load event multiple times
                // Even when the DOM is not ready yet
                // this fix should not affect other browsers
                return;
            }

            // fixing Opera 9.64
            if (doc.body && doc.body.innerHTML == "false") {
                // In Opera 9.64 event was fired second time
                // when body.innerHTML changed from false
                // to server response approx. after 1 sec
                return;
            }

            var response;

            if (doc.XMLDocument) {
                // response is a xml document Internet Explorer property
                response = doc.XMLDocument;
            } else if (doc.body) {
                // response is html document or plain text
                response = doc.body.innerHTML;

                if (settings.responseType && settings.responseType.toLowerCase() == 'json') {
                    // If the document was sent as 'application/javascript' or
                    // 'text/javascript', then the browser wraps the text in a <pre>
                    // tag and performs html encoding on the contents.  In this case,
                    // we need to pull the original text content from the text node's
                    // nodeValue property to retrieve the unmangled content.
                    // Note that IE6 only understands text/html
                    if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') {
                        response = doc.body.firstChild.firstChild.nodeValue;
                    }

                    if (response) {
                        response = eval("(" + response + ")");
                    } else {
                        response = {};
                    }
                }
            } else {
                // response is a xml document
                response = doc;
            }

            settings.onComplete.call(self, file, response);

            // Reload blank page, so that reloading main page
            // does not re-submit the post. Also, remember to
            // delete the frame
            toDeleteFlag = true;

            // Fix IE mixed content issue
            iframe.src = "javascript:'<html></html>';";
        });
    },
    /**
     * Upload file contained in this._input
     */
    submit: function () {
        var self = this,
            settings = this._settings;

        if (!this._input || this._input.value === '') {
            return;
        }

        var file = fileFromPath(this._input.value);

        // user returned false to cancel upload
        if (false === settings.onSubmit.call(this, file, getExt(file))) {
            this._clearInput();
            return;
        }

        // sending request
        var iframe = this._createIframe();
        var form = this._createForm(iframe);

        // assuming following structure
        // div -> input type='file'
        removeNode(this._input.parentNode);
        removeClass(self._button, self._settings.hoverClass);

        form.appendChild(this._input);

        form.submit();

        // request set, clean up
        removeNode(form);
        form = null;
        removeNode(this._input);
        this._input = null;

        // Get response from iframe and fire onComplete event when ready
        this._getResponse(iframe, file);

        // get ready for next request
        this._createInput();
    }
};

// module.exports= AjaxUpload;